%%--- coding:utf-8 ---
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% File Name: mc_manager_logic
%%% Created on : 2024/9/3 8:34
%%% @author Gaylen 252323463@qq.com
%%% @copyright (C) 2024, freedom
%%% @doc
%%%
%%% @end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-module(mc_manager_logic).
-author("Gaylen").
-include("mongodb_driver.hrl").

%% API
-export([
    init/1,
    stop/1,
    worker_process_check/1,
    pool_process_check/1,
    exit_process/2,

    sync_fetch_worker_and_pool/1,
    async_recycle_pool/2
]).

init(Options) ->
    erlang:process_flag(trap_exit, true),
    WorkerSize = mc_utils:get_value(worker_size, Options, 10),
    MinPoolSize = mc_utils:get_value(min_pool_size, Options, 10),
    MaxPoolSize = mc_utils:get_value(max_pool_size, Options, unlimited),
    {MasterOptions, DbMode} = priv_get_master_options(Options),
    WorkerPidList = priv_start_worker_process(Options, WorkerSize, []),
    PoolPidList = priv_start_pool_process(MinPoolSize, []),
    WorkerTimerRef = erlang:send_after(60000, self(), {worker_check}),
    FreePoolTimerRef = erlang:send_after(60000, self(), {free_pool_check}),
    #mongo_manager_state{
        worker_size = WorkerSize,
        min_pool_size = MinPoolSize,
        max_pool_size = MaxPoolSize,
        db_mode = DbMode,
        org_options = Options,
        options = MasterOptions,
        worker_pid_list = WorkerPidList,
        free_pool_list = PoolPidList,
        busy_pool_list = [],
        worker_timer = WorkerTimerRef,
        free_pool_timer = FreePoolTimerRef
    }.

%% private
priv_get_master_options(Options) ->
    HostList = mc_utils:get_value('host_res', Options, undefined),
    if
        HostList =:= undefined ->
            {Options, ?MONGO_DB_MODE_SINGLE};
        true ->
            {MasterHostIp, MasterPort} = priv_get_master_host(Options, HostList),
            MasterOptions0 = mc_utils:set_value('host', MasterHostIp, Options),
            MasterOptions = mc_utils:set_value('port', MasterPort, MasterOptions0),
            {MasterOptions, ?MONGO_DB_MODE_MASTER_SLAVE}
    end.

%% private
priv_get_master_host(_Options, []) ->
    undefined;
priv_get_master_host(Options, [{Ip, Port} | LeftHostList]) ->
    MasterOptions0 = mc_utils:set_value('host', Ip, Options),
    MasterOptions = mc_utils:set_value('port', Port, MasterOptions0),
    {ok, Pid} = mc_worker_api:connect(MasterOptions),
    Database = mc_utils:get_value(database, Options, <<"admin">>),
    IsMaster = mc_worker_api:is_master(Pid, Database),
    if
        IsMaster ->
            {Ip, Port};
        true ->
            priv_get_master_host(Options, LeftHostList)
    end.

%% private
priv_start_worker_process(_Options, WorkerSize, WorkerPidList) when WorkerSize =< 0 ->
    WorkerPidList;
priv_start_worker_process(Options, WorkerSize, WorkerPidList) ->
    {ok, Pid} = mc_worker_api:connect(Options),
    priv_start_worker_process(Options, WorkerSize - 1, [Pid | WorkerPidList]).

%% private
priv_start_pool_process(PoolSize, PoolPidList) when PoolSize =< 0 ->
    PoolPidList;
priv_start_pool_process(PoolSize, PoolPidList) ->
    %% 启动工作进程
    {ok, Pid} = mc_pool:start_link(),
    priv_start_pool_process(PoolSize - 1, [Pid | PoolPidList]).

stop(State) ->
    #mongo_manager_state{
        worker_pid_list = WorkerPidList,
        free_pool_list = FreePoolList,
        busy_pool_list = BusyPoolList
    } = State,
    lists:foldl(
        fun(PoolPid, Acc0) ->
            %% 停止工作进程
            mc_pool:sync_close(PoolPid),
            Acc0
        end, ok, FreePoolList),
    lists:foldl(
        fun(PoolPid, Acc0) ->
            %% 停止工作进程
            mc_pool:sync_close(PoolPid),
            Acc0
        end, ok, BusyPoolList),
    lists:foldl(
        fun(WorkerPid, Acc0) ->
            mc_worker:disconnect(WorkerPid),
            Acc0
        end, ok, WorkerPidList),
    ok.

worker_process_check(State) ->
    %% 检查获取主节点配置并检查主节点有没有变化
    NewState0 = priv_check_master_options(State),
    #mongo_manager_state{
        worker_size = WorkerSize,
        worker_pid_list = WorkerPidList,
        options = Options
    } = NewState0,
    %% 检查mc_worker进程数是否小于配置的数量
    WorkerPidListLen = length(WorkerPidList),
    if
        WorkerPidListLen < WorkerSize ->
            NewWorkerPidList = priv_start_worker_process(Options, WorkerSize - WorkerPidListLen, WorkerPidList),
            NewState0#mongo_manager_state{
                worker_pid_list = NewWorkerPidList
            };
        true ->
            NewState0
    end.

priv_check_master_options(State) ->
    %% 检查最新的主节点的IP和端口是否与上一次的相同，不相同，则断开所有连接，更新主节点配置
    {MasterOptions, DbMode} = priv_get_master_options(State#mongo_manager_state.org_options),
    State#mongo_manager_state{
        db_mode = DbMode,
        options = MasterOptions
    }.

pool_process_check(State) ->
    %% 检查空闲的进程数是否大于配置的最小数量，是则缩小当前空闲数量的一半
    #mongo_manager_state{
        min_pool_size = MinPoolSize,
        free_pool_list = FreePoolList
    } = State,
    FreePoolListLen = length(FreePoolList),
    if
        FreePoolListLen > MinPoolSize ->
            DelFreePoolLen = max(min(FreePoolListLen div 2, FreePoolListLen - MinPoolSize), 1),
            NewFreePoolList = priv_stop_pool_process(DelFreePoolLen, FreePoolList),
            State#mongo_manager_state{
                free_pool_list = NewFreePoolList
            };
        true ->
            State
    end.

priv_stop_pool_process(DelFreePoolLen, FreePoolList) when DelFreePoolLen =< 0 ->
    FreePoolList;
priv_stop_pool_process(_DelFreePoolLen, []) ->
    [];
priv_stop_pool_process(DelFreePoolLen, [FreePoolPid | LeftFreePoolList]) ->
    mc_pool:close(FreePoolPid),
    priv_stop_pool_process(DelFreePoolLen - 1, LeftFreePoolList).

exit_process(ExitPid, State) ->
    %% 有进程退出
    #mongo_manager_state{
        worker_pid_list = WorkerPidList,
        free_pool_list = FreePoolList,
        busy_pool_list = BusyPoolList
    } = State,
    NewWorkerPidList = lists:delete(ExitPid, WorkerPidList),
    NewFreePoolList = lists:delete(ExitPid, FreePoolList),
    NewBusyPoolList = lists:delete(ExitPid, BusyPoolList),
    State#mongo_manager_state{
        worker_pid_list = NewWorkerPidList,
        free_pool_list = NewFreePoolList,
        busy_pool_list = NewBusyPoolList
    }.

%% 返回worker和pool进程Id
sync_fetch_worker_and_pool(State) ->
    #mongo_manager_state{
        worker_pid_list = WorkerPidList,
        free_pool_list = FreePoolList,
        busy_pool_list = BusyPoolList,
        max_pool_size = MaxPoolSize
    } = State,
    WorkerPidListLen = length(WorkerPidList),
    FreePoolListLen = length(FreePoolList),
    WorkerPid =
        if
            WorkerPidListLen > 0 -> lists:nth(rand:uniform(WorkerPidListLen), WorkerPidList);
            true -> undefined
        end,
    TotalPoolLen = length(BusyPoolList) + FreePoolListLen,
    if
        FreePoolListLen > 0 ->
            [FreePoolPid | LeftFreePoolList] = FreePoolList,
            NewState0 = State#mongo_manager_state{
                free_pool_list = LeftFreePoolList,
                busy_pool_list = [FreePoolPid | BusyPoolList]
            },
            {ok, NewState0, {ok, WorkerPid, FreePoolPid}};
        MaxPoolSize =:= unlimited orelse TotalPoolLen < MaxPoolSize ->
            {ok, NewPoolPid} = mc_pool:start_link(),
            NewState1 = State#mongo_manager_state{
                busy_pool_list = [NewPoolPid | BusyPoolList]
            },
            {ok, NewState1, {ok, WorkerPid, NewPoolPid}};
        true ->
            {ok, State, {error, <<"pool full">>}}
    end.

async_recycle_pool(State, PoolPid) ->
    IsProcessAlive = erlang:is_process_alive(PoolPid),
    if
        IsProcessAlive ->
            #mongo_manager_state{
                free_pool_list = FreePoolList,
                busy_pool_list = BusyPoolList
            } = State,
            NewFreePoolList = lists:reverse([PoolPid | lists:reverse(FreePoolList)]),
            NewBusyPoolList = lists:delete(PoolPid, BusyPoolList),
            NewState = State#mongo_manager_state{
                free_pool_list = NewFreePoolList,
                busy_pool_list = NewBusyPoolList
            },
            {ok, NewState, ok};
        true ->
            {ok, State, ok}
    end.
